package org.flashIso.engine.base {
	import flash.display.DisplayObject;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.utils.getDefinitionByName;
	
	import org.flashIso.engine.core.ILibrary;
	import org.flashIso.engine.core.IValidateable;
	import org.flashIso.engine.core.IsoObjectBase;
	import org.flashIso.engine.core.Point2D;
	import org.flashIso.engine.core.Point3D;
	import org.flashIso.engine.events.IsoEvent;
	import org.flashIso.engine.objects.IsoObject;
	import org.flashIso.engine.objects.IsoTile;
	
	/**
	 * 
	 * <p>IsoTiledMap is a container and manager for IsoTile and IsoObject objects. 
	 * Depth (zOrder) and flash coordinates are automaticaly adjusted according to 
	 * objects' position property and map's perspective.</p>
	 * <p>Use addElement and removeElement instead of addChild / removeChild.</p>
	 * <p>When perspective object changes draw method is called for all children.</p>
	 * 
	 * <p>Tiles are automatically arranged in cells.</p>
	 * <p>A cell can have at most one tile at a time.</p>
	 * <p>If a tile is added in a cell that that already has a tile attached, 
	 * the existing tile is automatically removed.</p>
	 * 
	 * <p>GridSize should only be used as a guide. It only affect the grid wireframe.
	 * This means that as long as the drawGrid property is set to 
	 * false, gridSize won't affect the display in any way.</p> 
	 * 
	 * <p>Use cellSize to adjust the size of cells.</p>
	 * 
	 */
	
	public class IsoTiledMap extends IsoMap
	{
		private var _cellSize:Point2D = new Point2D(50, 50);
		private var _gridSize:uint = 10;
		private var _drawGrid:Boolean = false;
		private var _tileHMap:Object;
		
		private var _tilesHMapInvalidated:Boolean = true;
		private var _tilesPositionInvalidated:Boolean = false;
		private var _gridInvalidated:Boolean = false;
		
		
		public function IsoTiledMap()
		{
			super();
			triggerValidation();
			_cellSize.atomicValidation = true;
			_cellSize.addEventListener(Event.CHANGE, gridChangedHandler, false, 0, true);
		}
		
		
		override protected function perspectiveChangeHandler(e:Event) : void {
			super.perspectiveChangeHandler(e);
			invalidateGrid();
		}
		
		protected function gridChangedHandler(e:Event) : void {
			invalidateGrid();
			invalidateTilesPosition();
			triggerValidation();
		}
		
		
		/**
		 * @return Cell at coordinates
		 **/
		public function getCellAtCoordinates(p:Point3D) : Point2D {
			var p2d:Point2D = new Point2D(Math.floor(p.x / _cellSize.x), Math.floor(p.y / _cellSize.y));
			return p2d;
		}
		
		/**
		 * @return Tile at 3D coordinates.
		 **/
		public function getTileAtCoordinates(p:Point3D) : IsoTile {
			var cell:Point2D = getCellAtCoordinates(p);
			return _tileHMap[cell.x+"-"+cell.y];
		}
		
		override protected function childPositionChangeHandler(e:Event) : void {
			if (e.target is IsoTile){
				trace("Tiles possition should not be changed. Use cell property instead.");
			}else{
				super.childPositionChangeHandler(e);
			}
		}
		
//		protected function setTileCellFromPosition(tile:IsoTile) : void {
//			tile.cell.autoValidation = false;
//			tile.cell = getCellAtCoordinates(tile.position);
//			tile.cell.autoValidation = true;
//			moveTileToCell(tile);
//		}
		
		protected function tileCellChangeHandler(e:Event) : void {
			var tile:IsoTile = e.target as IsoTile;
			setTileToCell(tile);
		}
		
		protected function setTileToCell(tile:IsoTile) : void {
			var oldTile:IsoTile = getTileAtCell(tile.cell);
			if (oldTile != null && oldTile != tile) {
				removeElement(oldTile);
			}
			setTilePositionFromCell(tile);
		}
		
		protected function setTilePositionFromCell(tile:IsoTile) : void {
			tile.position.autoValidation = false;
			tile.position.x = (tile.cell.x + 1) * _cellSize.x;
			tile.position.y = (tile.cell.y  + 1) *  _cellSize.y;
			tile.position.autoValidation = true;
			updateChildPosition(tile);
		}
		
		/**
		 * @return Tile at cell.
		 **/
		public function getTileAtCell(p:Point2D) : IsoTile {
			validateTilesHMapNow();
			return _tileHMap[p.x + "-" + p.y];
		}
		
		/**
		 * Use this method to remove all tiles from map
		 **/
		public function removeAllTiles() : void {
			var i:uint;
			var tiles:Vector.<IsoTile> = getTiles();
			
			for (i = 0; i < tiles.length; i++){
				removeElement(tiles[i]);
			}
		}
		
		/**
		 * @return Vector with all tiles in map.
		 **/
		public function getTiles() : Vector.<IsoTile> {
			var i:uint;
			var tiles:Vector.<IsoTile> = new Vector.<IsoTile>(); 
			
			for (i = 0; i < numChildren; i++){
				if (getChildAt(i) is IsoTile){
					tiles.push(getChildAt(i));
				}
			}
			return tiles;
		}
		
		/**
		 * Cell size should be equal to tiles size.
		 **/ 
		public function set cellSize(p:Point2D) : void {
			if (_cellSize.equals(p) == false){
				_cellSize.autoValidation = false;
				_cellSize.x = p.x;
				_cellSize.y = p.y;
				_cellSize.autoValidation = true;
				invalidateTilesPosition();
			}
		}
		
		public function get cellSize() : Point2D {
			return _cellSize;
		}

		/**
		 * 
		 * GridSize should only be used as a guide. It only affect the grid wireframe.
		 * This means that as long as the drawGrid property is set to 
		 * false, gridSize won't affect the display in any way. 
		 * 
		 **/
		public function set gridSize(value:uint) : void {
			if (_gridSize != value){
				_gridSize = value;
				invalidateGrid();
			}
		}
		
		public function get gridSize() : uint {
			return _gridSize;
		}

		/**
		 * Draw grid wireframe
		 * Should only be used as a guide in the development process.
		 * 
		 **/
		public function set drawGrid(value:Boolean) : void {
			if (_drawGrid != value){
				_drawGrid = value;
				invalidateGrid();
			}
		}
		
		public function get drawGrid() : Boolean {
			return _drawGrid;
		}

		private function parseForPath(cell:Point2D, table:Object, finalCell:Point2D, 
									  eightDirections:Boolean, elevationTolerance:Number = 0, 
									  level:uint = 0) : Boolean {
			
			var parseNext:Vector.<Point2D> = new Vector.<Point2D>();
			var nc:Point2D;
			var nextTile:IsoTile;
			var tile:IsoTile;
			var i:uint;
			var directions:Array = [[0,1],[0,-1],[1,0],[-1,0]]
			if (eightDirections == true){
				directions.push([1,1],[1,-1],[-1,1],[-1,-1]);
			}

			table[cell.x + "-" + cell.y] = 0;
			
			parseNext.push(cell);
			while (parseNext.length > 0){
				level = table[parseNext[0].x + "-" + parseNext[0].y]
				if(parseNext[0].equals(finalCell)){
					return true;
				}
				for (i=0; i<directions.length;i++){
					nc = parseNext[0].clone(directions[i][0],directions[i][1]);
					if ((table[nc.x + "-" + nc.y] == undefined) || (table[nc.x + "-" + nc.y] > (level + 1))){
						nextTile = getTileAtCell(nc);
						tile = getTileAtCell(parseNext[0]);
						if (nextTile != null && nextTile.walkable) {
							//elevation condition
							if (checkElevationCondition(tile, nextTile, directions[i][0], directions[i][1], elevationTolerance)) { 
								table[nc.x + "-" + nc.y] = level + 1;
								parseNext.push(nc);
							}
						}
					}
				}
				parseNext.shift();
			}
			return false;
		}
		
		private function checkElevationCondition(t1:IsoTile, t2:IsoTile, dx:Number, dy:Number, elevationTolerance:Number) : Boolean {
			if ((dx==1) && (dy==0) && (Math.abs(t1.elevationNorth - t2.elevationWest) <= elevationTolerance) && (Math.abs(t1.elevationEast - t2.elevationSouth) <= elevationTolerance)){
				return true;
			}
			if ((dx==-1) && (dy==0) && (Math.abs(t1.elevationWest - t2.elevationNorth) <= elevationTolerance) && (Math.abs(t1.elevationSouth - t2.elevationEast) <= elevationTolerance)){
				return true;
			}
			if ((dx==0) && (dy==1) && (Math.abs(t1.elevationNorth - t2.elevationEast) <= elevationTolerance) && (Math.abs(t1.elevationWest - t2.elevationSouth) <= elevationTolerance)){
				return true;
			}
			if ((dx==0) && (dy==-1) && (Math.abs(t1.elevationEast - t2.elevationNorth) <= elevationTolerance) && (Math.abs(t1.elevationSouth - t2.elevationWest) <= elevationTolerance)){
				return true;
			}
			
			if (dx==1 && dy==1 && Math.abs(t1.elevationNorth - t2.elevationSouth) <= elevationTolerance){
				return true;
			}
			if (dx==-1 && dy==-1 && Math.abs(t1.elevationSouth - t2.elevationNorth) <= elevationTolerance){
				return true;
			}
			if (dx==-1 && dy==1 && Math.abs(t1.elevationWest - t2.elevationEast) <= elevationTolerance){
				return true;
			}
			if (dx==1 && dy==-1 && Math.abs(t1.elevationEast - t2.elevationWest) <= elevationTolerance){
				return true;
			}
			return false;
		}
		
		private function getPathFromTable(startCell:Point2D, endCell:Point2D, table:Object, eightDirections:Boolean): Vector.<Point2D>{
			var v:Vector.<Point2D> = new Vector.<Point2D>();
			var i:int;
			var j:int;
			var nc:Point2D;
			while (!startCell.equals(endCell)){
				v.unshift(endCell);
				var directions:Array = [[0,1],[0,-1],[1,0],[-1,0]]
				if (eightDirections == true){
					directions.push([1,1],[1,-1],[-1,1],[-1,-1]);
				}
				for (i = 0; i < directions.length; i++){
					nc = endCell.clone(directions[i][0], directions[i][1]);
					if (table[nc.x + "-" + nc.y] != undefined){
						if (table[nc.x + "-" + nc.y] - table[endCell.x + "-" + endCell.y] == -1){
							endCell = nc;
							break;
						}	
					}
				}
			}
			v.unshift(endCell);
			return v;
		}
		
		
		/**
		 * 
		 * <p>Determines the shortest path between startCell and endCell using the FILL algorithm.</p>
		 * <p>Two consecutive tiles in path may not have the elevation difference grater that elevationTolerance.</p>
		 * <p>Tiles with walkable flag set to false will not be included in the path.</p>
		 * <p>Returns null if no path was found.</p>
		 **/
		public function getPath(startCell:Point2D, endCell:Point2D, elevationTolerance:Number=0, eightDirections:Boolean = false) : Vector.<Point2D>{
			var parseTable : Object = new Object();
			
			if (parseForPath(startCell, parseTable, endCell, eightDirections, elevationTolerance, 0)){
				return getPathFromTable(startCell, endCell, parseTable, eightDirections);
			}
			return null;
		}
		
		private function invalidateTilesHMap() : void {
			_tilesHMapInvalidated = true;
			triggerValidation();
		}
		
		private function validateTilesHMapNow() : void {
			if (_tilesHMapInvalidated){
				_tilesHMapInvalidated = false;
				_tileHMap = new Object();
				var i:uint;
				var tiles:Vector.<IsoTile> = getTiles();
				for (i = 0; i < tiles.length; i++){
					_tileHMap[tiles[i].cell.x + "-" + tiles[i].cell.y] = tiles[i]
				}
			}
		}
		
		private function invalidateTilesPosition() : void {
			_tilesPositionInvalidated = true;
			triggerValidation();
		}
		
		private function validateTilesPositionNow() : void {
			if (_tilesPositionInvalidated){
				_tilesPositionInvalidated = false;
				var tiles:Vector.<IsoTile> = getTiles();
				var i:int;
				for (i = 0; i < tiles.length; i++){
					setTilePositionFromCell(tiles[i]);
				}
			}
		}
		
		private function invalidateGrid() : void {
			_gridInvalidated = true;
			triggerValidation();
		}
		
		private function validateGridNow() : void {
			if (_gridInvalidated) {
				_gridInvalidated = false;
				graphics.clear();
				if (_drawGrid) {
					graphics.lineStyle(1, 0, 1);
					var i:int;
					for (i = 0; i < _gridSize + 1; i++) {
						var p3d:Point3D = new Point3D(0, i * _cellSize.y);
						var p2d:Point2D = perspective.getProjection(p3d);
						graphics.moveTo(p2d.x, p2d.y);
						p3d = new Point3D(_cellSize.x * _gridSize, i * _cellSize.y);
						p2d = perspective.getProjection(p3d); 
						graphics.lineTo(p2d.x, p2d.y);
					}
					for (i = 0; i < _gridSize + 1; i++) {
						p3d = new Point3D(i * _cellSize.x, 0); 
						p2d = perspective.getProjection(p3d);
						graphics.moveTo(p2d.x, p2d.y);
						p3d = new Point3D(i * _cellSize.x, _cellSize.y * _gridSize);
						p2d = perspective.getProjection(p3d); 
						graphics.lineTo(p2d.x, p2d.y);
					}
				}
			}
		}
		
		override public function validateNow() : void {
			validateGridNow();
			validateTilesHMapNow();
			validateTilesPositionNow();
			super.validateNow();
		}
		
		override public function addElement(child : IsoObjectBase) : void {
			super.addElement(child);
			if (child is IsoTile){
				(child as IsoTile).addEventListener(IsoEvent.CELL_CHANGE, tileCellChangeHandler, false, 0, true);
				setTileToCell(child as IsoTile);
				invalidateTilesHMap();
			}
		}
		
		override public function removeElement(child:IsoObjectBase) : void {
			super.removeElement(child);
			if (child is IsoTile){
				(child as IsoTile).removeEventListener(IsoEvent.CELL_CHANGE, tileCellChangeHandler);
				invalidateTilesHMap();
			}
		}
		
		override public function toXML() : XML {
			var xml:XML = super.toXML();
			
			xml.@cellSizeX = cellSize.x;
			xml.@cellSizeY = cellSize.y;
			return xml;
		}
		
		override public function fromXML(xml:XML, library:ILibrary = null) : void {
			super.fromXML(xml, library);
			
			cellSize.x = parseFloat(xml.@cellSizeX);
			cellSize.y = parseFloat(xml.@cellSizeY);
		}
	}
}